package org.krivtsun.scanlab.editor;

import org.krivtsun.scanlab.ScanLabApp;
import org.krivtsun.scanlab.utils.Functions;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.*;
import java.util.Map;
import java.util.HashMap;

/**
 * todo While switching from image to image - keep current image mode (scale???)
 * todo Limit scale increasing (and reducing) to 200% ???
 * next Add ability to change scale by mouse wheel (per +/-10%) 
 *
 * @author Andrew Krivtsun
 */
class ScaleManager {

    enum ScaleStyle {P200, P100, P50, P25, FIT_TO_WIDTH, FIT_TO_HEIGHT, BEST_FIT, ZOOM_IN, ZOOM_OUT};

    private final ChangeEvent STATE_CHANGED_EVENT = new ChangeEvent(this);

    private final Component comp;
    private final ComponentListener compListener;
    private boolean wasSizeTracking = false;

    private final Map<ScaleStyle, Action> map;
    private AbstractScaleAction lastAction;
    private JMenu menu;
    private JRadioButtonMenuItem emptyButton = new JRadioButtonMenuItem();

    private RenderedImage img;

    private double scale;
    private ChangeListener changeListener;

    private JLabel statusBarItem = new JLabel();

    ScaleManager(Component comp) {
        this.comp = comp;
        map = createActionMap();
        compListener = new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                assert lastAction != null;
                lastAction.executeAction();
            }
        };
    }

    private Map<ScaleStyle, Action> createActionMap() {
        Map<ScaleStyle, Action> map = new HashMap<ScaleStyle, Action>();
        map.put(ScaleStyle.P200, new PersentAction(200, '0'));
        map.put(ScaleStyle.P100, new PersentAction(100, '1'));
        map.put(ScaleStyle.P50, new PersentAction(50, '5'));
        map.put(ScaleStyle.P25, new PersentAction(25, '2'));
        map.put(ScaleStyle.FIT_TO_WIDTH, new FitToWidthAction());
        map.put(ScaleStyle.FIT_TO_HEIGHT, new FitToHeightAction());
        map.put(ScaleStyle.BEST_FIT, new BestFitAction());
        map.put(ScaleStyle.ZOOM_IN, new ZoomInAction());
        map.put(ScaleStyle.ZOOM_OUT, new ZoomOutAction());
        return map;
    }

    JMenu getMenu() {
        if (menu == null) {
            menu = createMenu();
        }
        return menu;
    }

    private JMenu createMenu() {
        JRadioButtonMenuItem fitToWidthButton = new JRadioButtonMenuItem(map.get(ScaleStyle.FIT_TO_WIDTH));
        JRadioButtonMenuItem fitToHeightButton = new JRadioButtonMenuItem(map.get(ScaleStyle.FIT_TO_HEIGHT));
        JRadioButtonMenuItem bestFitButton = new JRadioButtonMenuItem(map.get(ScaleStyle.BEST_FIT));

        ButtonGroup group = new ButtonGroup();
        group.add(fitToWidthButton);
        group.add(fitToHeightButton);
        group.add(bestFitButton);
        group.add(emptyButton);

        JMenu menu = new JMenu("Scale");
        menu.setMnemonic('s');
        menu.add(map.get(ScaleStyle.P200));
        menu.add(map.get(ScaleStyle.P100));
        menu.add(map.get(ScaleStyle.P50));
        menu.add(map.get(ScaleStyle.P25));
        menu.addSeparator();
        menu.add(fitToWidthButton);
        menu.add(fitToHeightButton);
        menu.add(bestFitButton);
        menu.addSeparator();
        menu.add(map.get(ScaleStyle.ZOOM_IN));
        menu.add(map.get(ScaleStyle.ZOOM_OUT));
        return menu;
    }

    void setImage(RenderedImage img) {
        this.img = img;
        for (Action action: map.values()) {
            action.setEnabled(img != null);
        }
        setScale(1);
    }

    double getScale() {
        return scale;
    }

    private void setScale(double scale) {
        this.scale = scale;
        updateStatusBar();
        fireStateChanged();
    }

    void setScaleChangeListener(ChangeListener listener) {
        this.changeListener = listener;
    }

    JComponent getStatusBarItem() {
        return statusBarItem;
    }

    private void enableSizeTracking(boolean enable) {
        if (enable) {
            if (!wasSizeTracking) {
                comp.addComponentListener(compListener);
                wasSizeTracking = true;
            }
        }
        else {
            if (wasSizeTracking) {
                comp.removeComponentListener(compListener);
                wasSizeTracking = false;
            }
        }
    }

    private void fireStateChanged() {
        if (changeListener != null) {
            changeListener.stateChanged(STATE_CHANGED_EVENT);
        }
    }

    private void clearFitStyleSelection() {
        emptyButton.doClick();
    }

    private void updateStatusBar() {
        statusBarItem.setText(img == null
                ? null
                : String.format("%.1f%%", getScale()*100));
    }

    private abstract class AbstractScaleAction extends AbstractAction {

        private final boolean shouldStartSizeTracking;

        AbstractScaleAction(String name, char mnemonic, boolean shouldStartSizeTracking) {
            super(name);
            putValue(Action.MNEMONIC_KEY, new Integer(mnemonic));
            this.shouldStartSizeTracking = shouldStartSizeTracking;
            setEnabled(false);
        }

        AbstractScaleAction(String name, char mnemonic, String icon, boolean shouldStartSizeTracking) {
            this(name, mnemonic, shouldStartSizeTracking);
            putValue(Action.SMALL_ICON, Functions.loadIcon(icon));
        }

        public void actionPerformed(ActionEvent e) {
            enableSizeTracking(shouldStartSizeTracking);
            executeAction();
            lastAction = this;
        }

        protected abstract void executeAction();
    }

    private class PersentAction extends AbstractScaleAction {

        private final int percent;

        PersentAction(int percent, char mnemonic) {
            super(percent + "%", mnemonic, false);
            this.percent = percent;
        }

        protected void executeAction() {
            setScale(percent/100d);
            clearFitStyleSelection();
        }
    }

    private class FitToWidthAction extends AbstractScaleAction {

        FitToWidthAction() {
            super("Fit to Width", 'W', "fitToWidth", true);
        }

        protected void executeAction() {
            double width = comp.getWidth(); // convert width from int to double for correct div in next line
            setScale(width/img.getWidth());
        }
    }

    private class FitToHeightAction extends AbstractScaleAction {

        FitToHeightAction() {
            super("Fit to Height", 'H', "fitToHeight", true);
        }

        protected void executeAction() {
            double height = comp.getHeight(); // convert height from int to double for correct div in next line
            setScale(height/img.getHeight());
        }
    }

    private class BestFitAction extends AbstractScaleAction {

        BestFitAction() {
            super("Best Fit", 'B', "bestFit", true);
        }

        protected void executeAction() {
            // convert sizes from int to double for correct division later
            double width = comp.getWidth();
            double height = comp.getHeight();
            double widthRatio = width/img.getWidth();            
            double heightRatio = height/img.getHeight();
            setScale(heightRatio > widthRatio? widthRatio: heightRatio);
        }
    }

    private class ZoomInAction extends AbstractScaleAction {

        ZoomInAction() {
            super("Zoom In", 'I', "zoomIn", false);
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_ADD, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
        }

        protected void executeAction() {
            setScale(getScale()*2);
            clearFitStyleSelection();
        }
    }

    private class ZoomOutAction extends AbstractScaleAction {

        ZoomOutAction() {
            super("Zoom Out", 'O', "zoomOut", false);
            putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
        }

        protected void executeAction() {
            setScale(getScale()/2);
            clearFitStyleSelection();
        }
    }
}
